package com.molamil.perilla {
	import flash.events.ProgressEvent;
	/**
	 * @author martindyrby
	 */

	import flash.display.DisplayObject;
	import flash.display.DisplayObjectContainer;
	import flash.external.ExternalInterface;
	import flash.utils.describeType;
	import flash.utils.getQualifiedClassName;


	public class Perilla {
		
		

		
		private static var socket : PerillaSocket;
		private static const PORT:int = 65000;
		private static const LOCALHOST:String = "127.0.0.1";
		private static var logCallback : Function;
		private static var _midis:Object;
		private static var instance : Perilla;
/*		
		private var rootObj: *;
		private var typeDefinitionCallback : String;
		private var removeDefinitionCallback : String;
		private var editPropertyCallback : String;
		private var valueCallback : String;
		private var objectIdCallback : String;	
		private var registeredObjects:Object;
*/
		
		public static function init(host:String = "127.0.0.1", port:int = 65000, root : * = null) : void {
			if(instance != null) return; 
			instance = new Perilla(host, port, root);
		}
		
		public function Perilla(host:String=null, port:int = -1, root:* = null) {
			if(instance != null) throw new Error("This is a singleton class.");

			socket = new PerillaSocket(host==null?LOCALHOST:host, port==-1?PORT:port);
			socket.addEventListener(ProgressEvent.SOCKET_DATA, socketDataHandler, false, 0, true);
			
			_midis = {};
		}

		private function socketDataHandler(event : ProgressEvent) : void {
			var response:String = socket.readResponse();
			var parts:Array = response.split(';');
			switch(parts[0]) {
				case 'midi':
					var name:String = parts[1];
					var value:Number = parts[2];
					var midi:Object = _midis[name];
					if(midi != null) {
						midi.callback.apply(null, [value]);
					}
					break;
			}
			
			
			log(socket.readResponse());
		}
		
		public static function set logFunction(value:Function):void {
			logCallback = value;
		}
		
		private function log(string : String) : void {
			if(logCallback != null) logCallback.apply(null, [string]);
		}
		
		
		/* INTERFACE */
		public static function addMidiCallback(name:String, midiOrShortcut:String, callback:Function, minimum:Number, maximum:Number):void {
			_midis[name] = {
				midi:midiOrShortcut,
				callback: callback,
				minimum: minimum,
				maximum: maximum
			};

			socket.write("midi;"+name+";"+midiOrShortcut+";"+minimum+";"+maximum);
		}
		
		static public function noteOn(note:String):String {
			return "144-"+getNoteValue(note);
		} 
		static public function noteOff(note:String):String {
			return "128-"+getNoteValue(note);
		}
		static public function control(param:int):String {
			return "176-"+param;
		}
		
		static public function programChange(param:int):String {
			return "192-"+param;
		}
		static public function getNoteValue(note:String):String {
			note = note.toLowerCase();
			
			if(isNaN(Number(note))) {
				var c:String = note.charAt(0);
				var sharp:String = note.charAt(1);
				var n:String;
				switch(c) {
					case 'a':
						n = sharp == "#" ? "10" : "9";
						break;
					case 'b':
						n = "11";
					case 'c':
						n = sharp == "#" ? "1" : "0";
						break;
					case 'd':
						n = sharp == "#" ? "3" : "2";
						break;
					case 'e':
						n = "4";
						break;
					case 'f':
						n = sharp == "#" ? "6" : "5";
						break;
					case 'g':
						n = sharp == "#" ? "8" : "7";
						break;
				}
				if(sharp == "#") {
					sharp = note.charAt(2);
				}
				var octave:int;
				if(sharp == "-") {
					sharp = note.charAt(3);
					octave = int(sharp) + 2;	
				} else {
					octave = int(sharp);
				}
				
				return "" + octave*12 + n;
			}
			
			return note;
		}
		
/*
 		private function privateInit(root:*):void {
			rootObj = root;
			registeredObjects = new Object();
			setupOutboundCommunication();
			setupInboundCommunication();
	
		}
*/		

		/*
		{
			target: Object,
			callback: Function,
			midiValue: int (0 - 127),
			midiType: int (0 - 127),
			minimum: Number (defaults to 0),
			maximum: Number (defaults to 1)
		}
		*/
/*
		public static function register(name:String, options : Object) : void {
			instance.registeredObjects[name] = options;
			if(options['target'] != null) {
				instance.sendTypeDef(options['target'], name);
			}
		}
		
		public static function unregister(name:String) : void {
			instance.sendRemoveTypeDef(name);
		}

		private function setupInboundCommunication() : void {
			if(ExternalInterface.available) {
				ExternalInterface.addCallback("changeNumeric", onChangeNumeric);
				log("listen for changeNumeric");
				ExternalInterface.addCallback("setSimple", onSetSimple);
				log("listen for onSetSimple");
				ExternalInterface.addCallback("setBoolean", onSetBoolean);
				log("listen for onSetBoolean");
				ExternalInterface.addCallback("addNumeric", onAddNumeric);
				log("listen for addNumeric");
				ExternalInterface.addCallback("changeString", onChangeString);
				log("listen for changeString");
				ExternalInterface.addCallback("requestTypeDef", onRequestTypeDef);
				log("listen for requestTypeDef");
				ExternalInterface.addCallback("requestValue", onRequestValue);
				log("listen for requestValue");
				ExternalInterface.addCallback("executeMethod", onMethod);
				log("listen for executeMethod");
				ExternalInterface.addCallback("executeBooleanMethod", onBooleanMethod);
				log("listen for executeBooleanMethod");
				ExternalInterface.addCallback("executeMethodWithParams", onParamMethod);
				log("listen for executeMethodWithParams");
			}
		}

		private function onRequestTypeDef(path:String) : void {
			log('onRequestTypeDef: ' + (onRequestTypeDef));
			var obj : * = translatePath(path);
			log('obj: ' + (obj));

 			sendTypeDef(obj, path);
		}

		private function log(string : String) : void {
			if(logCallback != null) logCallback.apply(null, [string]);
		}
		
		private function onRequestValue(path:String):void{
			var value:* = translatePath(path);
			log(value);
			if(ExternalInterface.available) {
				log(valueCallback+","+value+","+path);
				ExternalInterface.call(valueCallback, value, path);
			}
		}

		private function sendTypeDef(obj : *, path:String) : void {
			if(socket.connected) {
				var type:XML = describeType(obj);
				
				type.@path = path;
				
				if(obj is DisplayObjectContainer){
					addChildren(type,obj);
				}
				var msg:String = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"+type.toXMLString();
				
				log(msg);

				socket.write(msg);
			}
		}
		
		private function sendRemoveTypeDef(name:String):void {
			if(ExternalInterface.available) ExternalInterface.call(removeDefinitionCallback, name);
		}
		
		private function sendEditProperty(id : String, target : Object, propertyName : String) : void {
			if(ExternalInterface.available) {
				var type:XML = describeType(target);
				
				type.@path = id;			
				log("sendEditProperty("+id+","+target+", "+propertyName +")");
				ExternalInterface.call(editPropertyCallback, propertyName, type.toXMLString());
			}
		}


		
		private function addChildren(type : XML, obj : DisplayObjectContainer) : void {
			var numChildren:int = obj.numChildren;

			for (var i : int = 0; i < numChildren; ++i){
				var child:DisplayObject = obj.getChildAt(i);
				var xml:XML = new XML('<child name="'+child.name+'" type="'+getQualifiedClassName(child)+'" />');
				type.appendChild(xml);
			}
		}

		private function onSetSimple(path:String, property:String, value:String) : void {
			var obj : * = translatePath(path);
			obj[property] = value;
			sendCurrentValue(path, property, obj[property]);
		}
		
		private function onSetBoolean(path:String, property:String, value:String) : void {
			var obj : * = translatePath(path);
			obj[property] = value.toLowerCase() == "true" || value == "1";
			sendCurrentValue(path, property, obj[property]);
		}
		
		private function onChangeString(path:String, property:String, value:String) : void {
			var obj : * = translatePath(path);
			obj[property] = value;
			sendCurrentValue(path, property, obj[property]);
		}
				
		private function onChangeNumeric(path:String, property:String, value:String) : void {
			var obj : * = translatePath(path);
			obj[property] = Number(value);
			sendCurrentValue(path, property, obj[property]);
		}
		
		private function onAddNumeric(path:String, property:String, value:String) : void {
			var obj : * = translatePath(path);
			obj[property] += Number(value);
			sendCurrentValue(path, property, obj[property]);
		}
		
		private function onMethod(path:String, method:String) : void {
			var obj : * = translatePath(path);
			(obj[method] as Function).apply();
		}
		
		private function onBooleanMethod(path:String, method:String, value:String) : void {
			var obj : * = translatePath(path);
			var bool:Boolean = value == "true";
			(obj[method] as Function).apply(null, [bool]);
		}
		
		private function onParamMethod(path:String, method:String, ...params) : void {
			var obj : * = translatePath(path);
			(obj[method] as Function).apply(null, params);
		}
		

		private function translatePath(path : String) : * {
			log("translatePath: "+path);
			var elements:Array = path.split(".");
			
			var obj:Object;
			if(registeredObjects.hasOwnProperty(elements[0])) {
				var regPath:String = elements.shift();
				log("regPath: "+regPath);
				obj = registeredObjects[regPath].target;
				log("obj: "+obj);
			} else {
				obj = rootObj; 
			}

			var nextPart:String;
			var dcObj:DisplayObjectContainer;
			var nextChild:DisplayObject;
			log("elements.length: "+elements.length);
			for (var i : int = 0; i < elements.length; ++i) {
				nextPart = elements[i];
				log("nextPart: "+nextPart);

				if (nextPart != '') {

					if (obj.hasOwnProperty(nextPart)){
						log("obj.hasOwnProperty(nextPart)");
						obj = obj[nextPart];
						continue;
					} else if(obj is DisplayObjectContainer){
						dcObj = DisplayObjectContainer(obj);
						nextChild = dcObj.getChildByName(nextPart);
						if (nextChild != null) {
							obj = nextChild;
							continue;
						} else {
							for(var j:int = 0; j < dcObj.numChildren;++j){
								nextChild = dcObj.getChildAt(j);
								if (nextChild.name == nextPart) {
									obj = nextChild;
									break;
								}
							}
							continue;
						}
					}
				}
			}
			
			return obj;
		}
		
		
		private function sendCurrentValue(path : String, property : String, value : *) : void {
			if(ExternalInterface.available) {
				ExternalInterface.call(valueCallback, value, "."+path+"."+property);
			}
		}

		private function setupOutboundCommunication() : void {
			typeDefinitionCallback = "addTypeDef";
			removeDefinitionCallback = "removeTypeDef";
			valueCallback = "asValueforPath";
			objectIdCallback = "asObjectId";
			editPropertyCallback = "asEditPropForType";
		}

		 */

	

	}
}

import flash.errors.IOError;
import flash.events.SecurityErrorEvent;
import flash.events.IOErrorEvent;
import flash.events.Event;
import flash.net.Socket;

class PerillaSocket extends Socket {
	private var hasResponse : Boolean;
	private var pending:Array;

    public function PerillaSocket(host:String = null, port:uint = 0) {
        super(host, port);
		pending = [];
        configureListeners();
        if (host && port)  {
            super.connect(host, port);
        }
    }

    private function configureListeners():void {
        addEventListener(Event.CLOSE, closeHandler);
        addEventListener(Event.CONNECT, connectHandler);
        addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
        addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
    }

    public function write(str:String):void {
		
		trace('write: ' + str);
		if (!hasResponse) {
			pending.push(str);
			return;
		}
        str += "\r\n";
        try {
			writeUTFBytes(str);
        }
        catch(e:IOError) {
            trace(e);
        }
    }
	
	override public function writeObject(object:*):void {
		if(!hasResponse) return;
		try {
			super.writeObject(object);
		}
		catch(e:IOError) {
			trace(e);
		}
	}

    public function readResponse():String {
        return readUTFBytes(bytesAvailable);
    }

    private function closeHandler(event:Event):void {
        trace("closeHandler: " + event);
    }

    private function connectHandler(event:Event):void {
		hasResponse = true;
		while(pending.length > 0) {
			write(pending.shift());
		}
        trace("connectHandler: " + event);
    }

    private function ioErrorHandler(event:IOErrorEvent):void {
        trace("ioErrorHandler: " + event);
    }

    private function securityErrorHandler(event:SecurityErrorEvent):void {
        trace("securityErrorHandler: " + event);
    }
	

}